package spitfire.ksim.algorithm;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import spitfire.ksim.util.TStopWatch;

/**
 * Algorithm to calculate the similarity score between a raw data and a {@link FuzzyRule}.
 * @author Adam
 *
 */
public class SimilarityAlgorithm {

	private static double calMean(List<Double> values) {
		double sum = 0;;
		for (double d : values) {
			sum += d;
		}
		
		return sum / values.size();
	}
	
	private static double calDeviation(List<Double> values, double mean) {
		double sum = 0;
		for (double d : values) {
			sum += (d - mean) * (d - mean);
		}
		return Math.sqrt(sum / values.size());
	}
	
	public static double calculateScore(List<Double> dataList, FuzzyRule rule) {
		if (rule.size() < 2) {
			throw new RuntimeException("size too small");
		}
//		double rawMax = Collections.max(dataList);
//		double rawMin = Collections.min(dataList);
		
	  //Benchmarking
		TStopWatch Benchmark = new TStopWatch();
		long Loop = 1000;
		long btime = 0;
		for (int loop=0; loop<Loop; loop++) {
			Benchmark.start();
			
			double rawMax = Collections.max(dataList);
			double rawMin = Collections.min(dataList);
			ArrayList<Double> xList = rule.getxList();
			ArrayList<Double> yList = rule.getyList();
			double ruleMax = rule.getrMax();
			double ruleMin = rule.getrMin();
			
			double drange = Math.abs(rawMin-ruleMin) + Math.abs(rawMax-ruleMax);
			double twiness = (rawMax - rawMin) / (ruleMax - ruleMin);
			if (twiness > 1) {
				twiness = 1.0 / twiness;
			}
			List<Double> procDataList = preprocess(dataList, rawMax, rawMin);
			
			double sumNDG1 = 0;
			for (Double dataValue : procDataList) {
				sumNDG1 += interpolate(dataValue, xList, yList);
			}
			
			double sumNDG2 = 0;
			List<Double> normXList = normalize(xList, ruleMax, ruleMin);
			List<Double> normProcDataList = normalize(procDataList, rawMax, rawMin);
			for (Double normDataValue : normProcDataList) {
				sumNDG2 += interpolate(normDataValue, normXList, yList);
			}
			
			double sc = sumNDG1 / procDataList.size() * Math.exp(-drange)
						+ sumNDG2 / normProcDataList.size() * twiness;
			
			Benchmark.stop();
			btime += Benchmark.getElapsedTime();
		}
		long BenchmarkTime = btime / Loop;
		System.out.println("Bechmark time: "+BenchmarkTime);
		//End benchmarking*/
		
		double mean = calMean(dataList);
		double deviation = calDeviation(dataList, mean);
		double rawMax = mean + 3 * deviation;
		double rawMin = mean - 3 * deviation;
		
		ArrayList<Double> xList = rule.getxList();
		ArrayList<Double> yList = rule.getyList();
		double ruleMax = rule.getrMax();
		double ruleMin = rule.getrMin();
		
		double drange = Math.abs(rawMin-ruleMin) + Math.abs(rawMax-ruleMax);
		double twiness = (rawMax - rawMin) / (ruleMax - ruleMin);
		if (twiness > 1) {
			twiness = 1.0 / twiness;
		}
		List<Double> procDataList = preprocess(dataList, rawMax, rawMin);
		
		double sumNDG1 = 0;
		for (Double dataValue : procDataList) {
			sumNDG1 += interpolate(dataValue, xList, yList);
		}
		
		double sumNDG2 = 0;
		List<Double> normXList = normalize(xList, ruleMax, ruleMin);
		List<Double> normProcDataList = normalize(procDataList, rawMax, rawMin);
		for (Double normDataValue : normProcDataList) {
			sumNDG2 += interpolate(normDataValue, normXList, yList);
		}
		
		double sc = sumNDG1 / procDataList.size() * Math.exp(-drange)
					+ sumNDG2 / normProcDataList.size() * twiness;
		return sc;
	}

	private static List<Double> normalize(List<Double> list,
			double max, double min) {
		double range = max - min;
		List<Double> normList = new ArrayList<Double>();
		for (Double d : list) {
			normList.add((d - min) / range);
		}
		return normList;
	}

	/**
	 * Linear interpolate the NDG value given a data value and an NDG curve.
	 * @param dataValue - x value of interpolate point.
	 * @param xList - list of x coordinates of the curve.
	 * @param yList - list of y coordinates of the curve.
	 */
	private static double interpolate(double dataValue,
			List<Double> xList, List<Double> yList) {
		int p1 = Collections.binarySearch(xList, dataValue);
		int p2 = 0;
		if (p1 >= 0 ) {
			// found
			return yList.get(p1);
		} else {
			// not found
			p1 = -p1 - 1;
			if (p1 == 0) {
				// smaller than min
				return 0;
			} else if (p1 == xList.size()) {
				// bigger than max
				return 0;
			} else {
				// data value between rule range
				p1--;
				p2 = p1 + 1;
				double x1 = xList.get(p1);
				double x2 = xList.get(p2);
				double y1 = yList.get(p1);
				double y2 = yList.get(p2);
				return (x1*y2-y1*x2)/(x1-x2) + (y2-y1)/(x2-x1)*dataValue;
			}
		}
	}

	/**
	 * Eliminate outliers and return filtered data list
	 * @param dataList - source data list
	 * @param rawMax - upper bound of valid data.
	 * @param rawMin - lower bound of valid data.
	 * @return data list without outliers.
	 */
	private static List<Double> preprocess(List<Double> dataList,
			double rawMax, double rawMin) {
		List<Double> procDataList = new ArrayList<Double>();
		for (Double dataValue : dataList) {
			// eliminate outliers
			if (dataValue >= rawMin && dataValue <= rawMax) {
				procDataList.add(dataValue);
			}
		}
		return procDataList;
	}
}
